eletrotupi / tcc/ frontend/app/care-actions/[id].tsx9897f0373a757f09dbe54e9fae5842f71c99703d
15.3 KB | 542 lines Raw
1
import { useState, useEffect } from "react";
2
import {
3
  StyleSheet,
4
  View,
5
  ScrollView,
6
  Pressable,
7
  ActivityIndicator,
8
} from "react-native";
9
import { Text } from "@/components/ui/Text";
10
import { useLocalSearchParams, router, Stack } from "expo-router";
11
import { MaterialIcons, Ionicons } from "@expo/vector-icons";
12
import { Card, SubtleInfoCard } from "@/components/ui/Cards";
13
import { Button } from "@/components/ui/Button";
14
import { Col, Row } from "@/components/ui/LayoutHelpers";
15
import { Section, SectionHeader } from "@/components/ui/Sections";
16
import { Badge } from "@/components/ui/Badge";
17
import { Typography, Theme, Colors, Spacing } from "@/constants/theme";
18
import {
19
  useCareAction,
20
  useDeleteCareAction,
21
} from "@/hooks/useCareAction.queries";
22
import { useTrigger } from "@/hooks/useTrigger.queries";
23
import { useMoodEntry } from "@/hooks/useMoodEntries.queries";
24
import { getActivityCategory } from "@/constants/care-action-categories";
25
import { getTriggerCategory } from "@/constants/trigger-categories";
26
import { getMood } from "@/constants/moods";
27
import { formatMoment } from "@/lib/utils/time";
28
import type { CareAction } from "@/lib/api";
29

30
// TODO: Generalize these into a single source of truth, similar to other
31
// constants
32

33
// label helpers
34
const APPOINTMENT_LABELS: Record<string, string> = {
35
  ANALYST: "Analista",
36
  PSYCHIATRIST: "Psiquiatra",
37
  GP: "Clínico geral",
38
  NUTRITIONIST: "Nutricionista",
39
  PHYSIOTHERAPIST: "Fisioterapeuta",
40
  OTHER: "Consulta",
41
};
42

43
const ACTIVITY_LABELS: Record<string, string> = {
44
  WALK: "Caminhada",
45
  YOGA: "Yoga",
46
  GYM: "Academia",
47
  MEDITATION: "Meditação",
48
  SOCIAL: "Momento social",
49
  CREATIVE: "Atividade criativa",
50
  OTHER: "Atividade",
51
};
52

53
const PERIODICITY_LABELS: Record<string, string> = {
54
  ONCE: "Dose única",
55
  DAILY: "1× ao dia",
56
  TWICE_DAILY: "2× ao dia",
57
  THREE_TIMES_DAILY: "3× ao dia",
58
  WEEKLY: "1× por semana",
59
  BIWEEKLY: "2× por semana",
60
  MONTHLY: "1× por mês",
61
};
62

63
// header meta
64
type HeaderMeta = {
65
  icon: keyof typeof MaterialIcons.glyphMap;
66
  color: string;
67
  iconBg: string;
68
  title: string;
69
};
70

71
function resolveHeader(ca: CareAction): HeaderMeta {
72
  switch (ca.type) {
73
    case "MEDICINE":
74
      return {
75
        icon: "medication",
76
        color: "#7C3AED",
77
        iconBg: "#EDE9FE",
78
        title: ca.medicineLog?.regimen?.name ?? "Medicamento",
79
      };
80
    case "APPOINTMENT":
81
      return {
82
        icon: "psychology",
83
        color: "#1D4ED8",
84
        iconBg: "#DBEAFE",
85
        title: `Consulta · ${APPOINTMENT_LABELS[ca.appointment?.type ?? ""] ?? "Consulta"}`,
86
      };
87
    case "ACTIVITY": {
88
      const meta = getActivityCategory(ca.activity?.type);
89
      return {
90
        icon: meta.icon,
91
        color: meta.color,
92
        iconBg: meta.iconBackground,
93
        title: ACTIVITY_LABELS[ca.activity?.type ?? ""] ?? "Atividade",
94
      };
95
    }
96
    default:
97
      return {
98
        icon: "star",
99
        color: "#6B7280",
100
        iconBg: "#F3F4F6",
101
        title: "Ação de cuidado",
102
      };
103
  }
104
}
105

106
// DetailRow
107
// TODO: Might as well generalize this into a component
108
type DetailRowProps = {
109
  icon: keyof typeof MaterialIcons.glyphMap;
110
  label: string;
111
  value: string;
112
};
113

114
function DetailRow({ icon, label, value }: DetailRowProps) {
115
  return (
116
    <Row style={styles.detailRow}>
117
      <View style={styles.detailIconWrap}>
118
        <MaterialIcons name={icon} size={18} color="#6366F1" />
119
      </View>
120
      <Col gap={1}>
121
        <Text style={styles.detailLabel}>{label}</Text>
122
        <Text style={styles.detailValue}>{value}</Text>
123
      </Col>
124
    </Row>
125
  );
126
}
127

128
export default function CareActionDetailScreen() {
129
  const { id } = useLocalSearchParams<{ id: string }>();
130
  const { data: careAction, isLoading, isError } = useCareAction(id);
131
  const deleteCareAction = useDeleteCareAction();
132

133
  const triggerId = String(careAction?.triggerId ?? "");
134
  const moodId = String(careAction?.moodId ?? "");
135
  const { data: trigger } = useTrigger(triggerId);
136
  const { data: mood } = useMoodEntry(moodId);
137

138
  const [minutesLeft, setMinutesLeft] = useState(0);
139
  const [canDelete, setCanDelete] = useState(false);
140

141
  useEffect(() => {
142
    const updateTimer = () => {
143
      const createdAt = careAction?.createdAt;
144
      if (!createdAt) return;
145

146
      const msSinceCreation = Date.now() - new Date(createdAt).getTime();
147
      const windowMs = 5 * 60 * 1000;
148

149
      setCanDelete(msSinceCreation < windowMs);
150
      setMinutesLeft(
151
        Math.max(0, Math.ceil((windowMs - msSinceCreation) / 60000)),
152
      );
153
    };
154

155
    if (isLoading) return;
156
    updateTimer();
157
    const interval = setInterval(updateTimer, 1000);
158
    return () => clearInterval(interval);
159
  }, [careAction, isLoading]);
160

161
  if (isLoading) {
162
    return (
163
      <View style={styles.centered}>
164
        <ActivityIndicator size="large" color={Colors.light.tint} />
165
      </View>
166
    );
167
  }
168

169
  if (isError || !careAction) {
170
    return (
171
      <View style={styles.centered}>
172
        <Text style={styles.errorText}>Registro não encontrado.</Text>
173
        <Pressable onPress={() => router.back()} style={styles.backButton}>
174
          <Text style={styles.backButtonText}>Voltar</Text>
175
        </Pressable>
176
      </View>
177
    );
178
  }
179

180
  const header = resolveHeader(careAction);
181

182
  // TODO: Componentize this as well? It's on each of the details screens
183
  const deleteButtonMsg = () => {
184
    const suffix = canDelete
185
      ? `(Disp. por ${minutesLeft < 1 ? "<1" : minutesLeft}:00)`
186
      : "(Expirado)";
187
    return `Excluir Registro ${suffix}`;
188
  };
189

190
  const handleDelete = async () => {
191
    await deleteCareAction.mutateAsync(String(careAction.id));
192
    router.back();
193
  };
194

195
  const hasLink = !!(careAction.triggerId || careAction.moodId);
196

197
  return (
198
    <>
199
      <Stack.Screen options={{ title: "Ver detalhes" }} />
200

201
      <ScrollView
202
        style={styles.container}
203
        contentContainerStyle={styles.content}
204
        showsVerticalScrollIndicator={false}
205
      >
206
        <Card style={{ padding: Spacing.cardGap }}>
207
          <Row style={{ alignItems: "center" }}>
208
            <View style={[styles.iconWrap, { backgroundColor: header.iconBg }]}>
209
              <MaterialIcons
210
                name={header.icon}
211
                size={22}
212
                color={header.color}
213
              />
214
            </View>
215
            <Col gap={2}>
216
              <Text style={styles.title}>{header.title}</Text>
217
              <Text style={styles.subtitle}>
218
                {formatMoment(new Date(careAction.moment))}
219
              </Text>
220
            </Col>
221
          </Row>
222
        </Card>
223

224
        <Card style={styles.topCard}>
225
          {careAction.type === "MEDICINE" && (
226
            <>
227
              {careAction.medicineLog?.regimen?.dosage ? (
228
                <DetailRow
229
                  icon="medication"
230
                  label="Dosagem"
231
                  value={careAction.medicineLog.regimen.dosage}
232
                />
233
              ) : null}
234
              {careAction.medicineLog?.regimen?.periodicity ? (
235
                <DetailRow
236
                  icon="schedule"
237
                  label="Frequência"
238
                  value={
239
                    PERIODICITY_LABELS[
240
                      careAction.medicineLog.regimen.periodicity
241
                    ] ?? careAction.medicineLog.regimen.periodicity
242
                  }
243
                />
244
              ) : null}
245
            </>
246
          )}
247

248
          {careAction.type === "APPOINTMENT" && (
249
            <>
250
              <DetailRow
251
                icon="psychology"
252
                label="Tipo"
253
                value={
254
                  APPOINTMENT_LABELS[careAction.appointment?.type ?? ""] ??
255
                  "Consulta"
256
                }
257
              />
258
              {careAction.appointment?.duration ? (
259
                <DetailRow
260
                  icon="timer"
261
                  label="Duração"
262
                  value={`${careAction.appointment.duration} min`}
263
                />
264
              ) : null}
265
            </>
266
          )}
267

268
          {careAction.type === "ACTIVITY" && (
269
            <>
270
              <DetailRow
271
                icon={header.icon}
272
                label="Tipo"
273
                value="Atividade"
274
              />
275
              {careAction.activity?.duration ? (
276
                <DetailRow
277
                  icon="timer"
278
                  label="Duração"
279
                  value={`${careAction.activity.duration} min`}
280
                />
281
              ) : null}
282
            </>
283
          )}
284
        </Card>
285

286
        {careAction.appointment?.note ? (
287
          <Section>
288
            <SectionHeader title="Anotações" variant="subtle" />
289
            <Card style={{ padding: Spacing.cardGap }}>
290
              <Text style={styles.annotation}>
291
                "{careAction.appointment.note.trim()}"
292
              </Text>
293
            </Card>
294
          </Section>
295
        ) : null}
296

297
        {hasLink && (
298
          <Section>
299
            <SectionHeader title="Vínculo" variant="subtle" />
300
            <Col gap={8}>
301
              {trigger && (
302
                <Pressable
303
                  onPress={() =>
304
                    router.push(`/triggers/${trigger.id}`)
305
                  }
306
                  style={({ pressed }) => [pressed && { opacity: 0.7 }]}
307
                >
308
                  <Card style={styles.linkCard}>
309
                    <Row style={{ alignItems: "center", gap: 12 }}>
310
                      {(() => {
311
                        const meta = getTriggerCategory(trigger.category);
312
                        return (
313
                          <View
314
                            style={[
315
                              styles.linkIconCircle,
316
                              { backgroundColor: meta.iconBackground },
317
                            ]}
318
                          >
319
                            <MaterialIcons
320
                              name={meta.icon}
321
                              size={20}
322
                              color={meta.color}
323
                            />
324
                          </View>
325
                        );
326
                      })()}
327
                      <Col gap={2} style={{ flex: 1 }}>
328
                        <Row style={{ alignItems: "center", gap: 6 }}>
329
                          <Text style={styles.linkLabel}>
330
                            {getTriggerCategory(trigger.category).label}
331
                          </Text>
332
                          <Badge label="Gatilho" variant="yellow" style={undefined} />
333
                        </Row>
334
                        <Text style={styles.linkTime}>
335
                          {formatMoment(new Date(trigger.moment))}
336
                        </Text>
337
                      </Col>
338
                      <Ionicons
339
                        name="chevron-forward"
340
                        size={16}
341
                        color={Colors.light.disabled}
342
                      />
343
                    </Row>
344
                  </Card>
345
                </Pressable>
346
              )}
347

348
              {mood && (
349
                <Pressable
350
                  onPress={() =>
351
                    router.push(`/entry/${mood.id}`)
352
                  }
353
                  style={({ pressed }) => [pressed && { opacity: 0.7 }]}
354
                >
355
                  <Card style={styles.linkCard}>
356
                    <Row style={{ alignItems: "center", gap: 12 }}>
357
                      <View style={styles.linkIconCircle}>
358
                        <Text style={styles.linkEmoji}>
359
                          {getMood(mood.selectedMood.toLowerCase())?.icon ?? "😐"}
360
                        </Text>
361
                      </View>
362
                      <Col gap={2} style={{ flex: 1 }}>
363
                        <Row style={{ alignItems: "center", gap: 6 }}>
364
                          <Text style={styles.linkLabel}>
365
                            {getMood(mood.selectedMood.toLowerCase())?.label ?? mood.selectedMood}
366
                          </Text>
367
                          <Badge label="Humor" variant="yellow" style={undefined} />
368
                        </Row>
369
                        <Text style={styles.linkTime}>
370
                          {formatMoment(new Date(mood.moment))}
371
                        </Text>
372
                      </Col>
373
                      <Ionicons
374
                        name="chevron-forward"
375
                        size={16}
376
                        color={Colors.light.disabled}
377
                      />
378
                    </Row>
379
                  </Card>
380
                </Pressable>
381
              )}
382
            </Col>
383
          </Section>
384
        )}
385

386
        <View style={styles.section}>
387
          <Text style={styles.sectionLabel}>GESTÃO DO REGISTRO</Text>
388
          <Button
389
            variant="danger"
390
            loading={deleteCareAction.isPending}
391
            disabled={!canDelete || deleteCareAction.isPending}
392
            onPress={handleDelete}
393
            title={deleteButtonMsg()}
394
          />
395

396
          <SubtleInfoCard
397
            style={styles.deleteInfo}
398
            text={`
399
              A exclusão de registros é permitida apenas nos primeiros 5 minutos
400
              após o envio para garantir a integridade do histórico emocional.
401
            `}
402
          />
403
        </View>
404
      </ScrollView>
405
    </>
406
  );
407
}
408

409
const styles = StyleSheet.create({
410
  container: {
411
    flex: 1,
412
    backgroundColor: Colors.light.background,
413
  },
414
  content: {
415
    padding: Spacing.containerPadding,
416
    gap: Spacing.sectionGap,
417
    paddingBottom: 48,
418
  },
419
  centered: {
420
    flex: 1,
421
    justifyContent: "center",
422
    alignItems: "center",
423
    gap: 12,
424
  },
425
  errorText: {
426
    fontSize: 15,
427
    color: Colors.light.textSecondary,
428
  },
429
  backButton: {
430
    paddingHorizontal: 20,
431
    paddingVertical: 10,
432
    backgroundColor: Colors.light.tint,
433
    borderRadius: Theme.borderRadius.md,
434
  },
435
  backButtonText: {
436
    fontWeight: "700",
437
    color: Colors.light.text,
438
  },
439

440
  // Top card
441
  topCard: {
442
    padding: 16,
443
    gap: 0,
444
  },
445
  iconWrap: {
446
    width: 40,
447
    height: 40,
448
    borderRadius: 20,
449
    alignItems: "center",
450
    justifyContent: "center",
451
    marginTop: 1,
452
  },
453
  title: {
454
    ...Typography.headlineLg,
455
    fontWeight: 700,
456
    color: Colors.light.text,
457
  },
458
  subtitle: {
459
    fontSize: 13,
460
    color: Colors.light.textSecondary,
461
  },
462
  divider: {
463
    height: 1,
464
    backgroundColor: Colors.light.divider,
465
    marginVertical: 12,
466
  },
467

468
  // Detail rows
469
  detailRow: {
470
    alignItems: "center",
471
    gap: 10,
472
    paddingVertical: 6,
473
  },
474
  detailIconWrap: {
475
    width: 32,
476
    height: 32,
477
    borderRadius: 8,
478
    backgroundColor: "#EEF2FF",
479
    alignItems: "center",
480
    justifyContent: "center",
481
  },
482
  detailLabel: {
483
    fontSize: 11,
484
    color: Colors.light.textSecondary,
485
    textTransform: "uppercase",
486
    letterSpacing: 0.5,
487
  },
488
  detailValue: {
489
    fontSize: 14,
490
    fontWeight: "700",
491
    color: Colors.light.text,
492
  },
493

494
  // Link cards
495
  linkCard: {
496
    padding: Spacing.cardGap,
497
  },
498
  linkIconCircle: {
499
    width: 40,
500
    height: 40,
501
    borderRadius: 20,
502
    alignItems: "center",
503
    justifyContent: "center",
504
    backgroundColor: "#F3F4F6",
505
  },
506
  linkLabel: {
507
    fontSize: 15,
508
    fontWeight: "700",
509
    color: Colors.light.text,
510
  },
511
  linkTime: {
512
    fontSize: 12,
513
    color: Colors.light.textSecondary,
514
  },
515
  linkEmoji: {
516
    fontSize: 22,
517
  },
518

519
  // Management
520
  section: {
521
    gap: 8,
522
  },
523
  sectionLabel: {
524
    fontSize: 11,
525
    fontWeight: "600",
526
    letterSpacing: 0.8,
527
    color: Colors.light.textSecondary,
528
    textTransform: "uppercase",
529
  },
530
  deleteInfo: {
531
    marginTop: 4,
532
  },
533

534
  // Annotation
535
  annotation: {
536
    fontSize: 14,
537
    lineHeight: 22,
538
    color: Colors.light.textSecondary,
539
    fontStyle: "italic",
540
    padding: 4,
541
  },
542
});